TypeScript 55.4%
Python 43.2%
SQL 1.2%
1import Link from "next/link";2import type { Metadata } from "next";3import { notFound } from "next/navigation";4import { PRIORITY_LABELS } from "@websensor/core/client";5import { EventRow } from "@/components/event-row";6import { Badge, Bar, Chip, Empty, ExtLink, Flag, HealthPill, PageHeader, Panel, Sparkline, Stat, Table, Td, TierBadge } from "@/components/ui";7import { api, type SensorRow } from "@/lib/api";8import { fmtDuration, fmtInt, fmtMs, fmtPct, fmtScore, relTime, typeLabel, untilTime, utcDateTime } from "@/lib/format";910export const dynamic = "force-dynamic";1112export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {13 const { id } = await params;14 const d = await api.source(id);15 if (!d) return { title: "Source not found" };16 return { title: `${d.source.name} — source intelligence`, description: d.source.description ?? `Sensors, quality, activity and events for ${d.source.name} (${d.source.domain}).`, alternates: { canonical: `/source/${d.source.id}` } };17}1819function qualityTone(v: number): "signal" | "warn" | "hot" | undefined {20 return v >= 80 ? "signal" : v >= 60 ? undefined : v >= 40 ? "warn" : "hot";21}2223/** Source intelligence page (spec §27, §48). */24export default async function SourcePage({ params }: { params: Promise<{ id: string }> }) {25 const { id } = await params;26 const d = await api.source(id);27 if (!d) notFound();28 const events = await api.events({ source: d.source.id, limit: 40 });29 const src = d.source;30 const a = d.activity ?? {};31 const q = d.quality;32 const score = a.activity_score ?? 0;33 const entityId = d.entities.find((e) => e.type === "organization" || e.id.startsWith("org_"))?.id;34 const daily = [...(d.daily ?? [])].sort((x, y) => x.day.localeCompare(y.day)).slice(-30);35 const sensorsUp = d.sensors.filter((s) => s.enabled && s.health === "UP").length;36 const rawTotal = q?.raw_changes ?? d.sensors.reduce((n, s) => n + (s.raw_changes ?? 0), 0);37 const meaningfulTotal = q?.meaningful_changes ?? d.sensors.reduce((n, s) => n + (s.meaningful_changes ?? 0), 0);38 const maxType = Math.max(1, ...(d.by_type ?? []).map((t) => t.n));39 const extra = src as typeof src & { llm_enabled?: boolean; discover?: Record<string, boolean>; fallback?: Record<string, boolean>; rate_limit_per_min?: number | null; terms_reviewed_at?: string | null };40 return (41 <>42 <PageHeader43 compact44 kicker={45 <span className="flex flex-wrap items-center gap-2">46 <TierBadge tier={src.tier} />47 <span>Tier {src.tier}</span>48 <span className="text-fg-subtle">·</span>49 <Link href={`/domain/${src.domain}`} className="font-mono hover:underline">{src.domain}</Link>50 {src.country && (51 <>52 <span className="text-fg-subtle">·</span>53 <Link href={`/country/${src.country.toLowerCase()}`} className="inline-flex items-center gap-1 font-mono hover:text-fg"><Flag code={src.country} /> {src.country}</Link>54 </>55 )}56 {src.first_party === false ? <Badge kind="external" /> : <Badge kind="first-party" />}57 {src.enabled === false && <Chip tone="warn" className="font-mono">DISABLED</Chip>}58 </span>59 }60 title={src.name}61 description={src.description ?? (src.first_party === false ? `Media / aggregator source: its reports are third-party evidence and never count as first-party confirmation.` : `Official channels of ${src.name}: every sensor below is an endpoint the organization itself publishes.`)}62 actions={63 <>64 {src.categories.map((c) => <Chip key={c} href={`/category/${c}`}>{c}</Chip>)}65 {entityId && <Link href={`/entity/${entityId}`} className="rounded-md border border-line bg-panel px-2.5 py-1 text-[12px] hover:border-line-strong">Entity & timeline →</Link>}66 <Link href={`/live?q=${encodeURIComponent(src.name)}`} className="hidden rounded-md border border-line bg-panel px-2.5 py-1 text-[12px] hover:border-line-strong sm:inline-block">Live →</Link>67 <ExtLink href={src.homepage ?? `https://${src.domain}`} className="text-[12px]">Website ↗</ExtLink>68 </>69 }70 />7172 <div className="panel mb-4 grid grid-cols-2 divide-x divide-y divide-line sm:grid-cols-3 lg:grid-cols-6 lg:divide-y-0">73 <Stat label="Quality score" value={q ? fmtScore(q.quality_score) : "—"} tone={q ? qualityTone(q.quality_score) : undefined} hint={q ? `${q.sensors_up ?? sensorsUp} / ${q.sensors ?? d.sensors.length} sensors up` : "not computed yet"} />74 <Stat label="Success rate" value={q ? fmtPct(q.success_rate) : "—"} tone={q && q.success_rate < 0.9 ? "warn" : undefined} hint={q?.checks_7d ? `${fmtInt(q.checks_7d)} checks · ${fmtInt(q.errors_7d)} errors · 7 d` : `${fmtInt(q?.total_runs)} checks total`} />75 <Stat label="Avg latency" value={fmtMs(q?.avg_latency_ms)} hint="across sensors" />76 <Stat label="Structured" value={q ? fmtPct(q.structured_share) : "—"} hint={q ? `${q.structured_sensors ?? 0} feeds / APIs / status` : undefined} />77 <Stat label="Usefulness" value={q ? fmtPct(q.usefulness) : "—"} hint={`${fmtInt(meaningfulTotal)} meaningful / ${fmtInt(rawTotal)} raw`} />78 <Stat label="Sensors" value={<span>{sensorsUp}<span className="text-fg-subtle"> / {d.sensors.length}</span></span>} hint={`${d.sensors.filter((s) => s.health !== "UP").length} not UP`} tone={sensorsUp < d.sensors.length ? "warn" : undefined} />79 </div>8081 <div className="grid gap-4 xl:grid-cols-[1fr_320px]">82 <div className="flex min-w-0 flex-col gap-4">83 <Panel title="30 days · daily activity" action={daily.length ? <span className="text-[11px] text-fg-subtle">{daily[0]!.day} → {daily[daily.length - 1]!.day}</span> : undefined}>84 {daily.length ? (85 <div className="grid gap-4 sm:grid-cols-3">86 {(87 [88 { label: "Checks", key: "checks", tone: "muted" },89 { label: "Raw changes", key: "raw_changes", tone: "info" },90 { label: "Events", key: "events", tone: "signal" },91 ] as const92 ).map((s) => {93 const values = daily.map((r) => Number(r[s.key] ?? 0));94 const sum = values.reduce((x, y) => x + y, 0);95 return (96 <div key={s.key} className="min-w-0">97 <div className="flex items-baseline justify-between">98 <span className="label">{s.label}</span>99 <span className="font-mono text-[12px] tabular">{fmtInt(sum)}</span>100 </div>101 <div className="mt-1 w-full overflow-hidden">102 <Sparkline values={values} width={300} height={40} tone={s.tone} />103 </div>104 </div>105 );106 })}107 </div>108 ) : (109 <Empty>Daily counters accumulate from the first check onward — nothing recorded for the last 30 days yet.</Empty>110 )}111 </Panel>112113 <Panel title={`Sensors · ${d.sensors.length}`} dense action={<span className="hidden text-[11px] text-fg-subtle sm:inline">P0 critical · P1 major · P2 specialized · P3 low urgency</span>}>114 {d.sensors.length === 0 ? (115 <Empty>No sensors yet — discovery validates feeds, sitemaps and status pages before creating sensors.</Empty>116 ) : (117 <Table head={["Sensor", "Type", "Tier", "Prio", "Status", "Health", "Polling", "Last / next", "HTTP", "Cond.", "24 h", "Snap.", "304 %", "Raw / mean.", ""]}>118 {d.sensors.map((s) => <SensorTr key={s.id} s={s} />)}119 </Table>120 )}121 </Panel>122123 <Panel title="Recent events" dense action={<Link href={`/live?q=${encodeURIComponent(src.name)}`} className="text-[11px] text-fg-subtle hover:text-fg">live →</Link>}>124 {events.items.length ? events.items.map((e) => <EventRow key={e.id} ev={e} showDate />) : <Empty>No meaningful events yet for this source.</Empty>}125 </Panel>126127 {d.discovery.length > 0 && (128 <Panel title={`Discovered endpoints · ${d.discovery.length}`} dense>129 <Table head={["Kind", "URL", "Evidence", "Items", "Value", "Status", "Found"]}>130 {d.discovery.map((c) => (131 <tr key={c.url} className="hover:bg-panel-2/60">132 <Td mono>{c.kind}</Td>133 <Td><span className="block max-w-[28rem] truncate font-mono text-[11.5px]" title={c.url}>{c.url}</span></Td>134 <Td className="text-fg-subtle">{c.evidence}</Td>135 <Td mono>{c.score?.itemCount ?? "—"}</Td>136 <Td mono>{c.score?.value !== undefined ? fmtScore((c.score.value ?? 0) * 100) : "—"}</Td>137 <Td><Chip tone={c.status === "promoted" ? "ok" : c.status === "rejected" ? "danger" : "default"}>{c.status}</Chip></Td>138 <Td mono className="whitespace-nowrap text-fg-subtle">{relTime(c.found_at)}</Td>139 </tr>140 ))}141 </Table>142 </Panel>143 )}144 </div>145146 <aside className="flex min-w-0 flex-col gap-4">147 <Panel title="Activity anomaly">148 <div className="flex items-baseline justify-between">149 <span className={`font-mono text-3xl font-semibold tabular ${score >= 70 ? "text-hot" : score >= 45 ? "text-high" : "text-fg"}`}>{fmtScore(score)}</span>150 <span className="label">{score >= 70 ? "ACTIVITY ANOMALY" : score >= 45 ? "ELEVATED" : "NORMAL"}</span>151 </div>152 <div className="mt-2"><Bar value={score} tone={score >= 70 ? "hot" : score >= 45 ? "high" : "signal"} /></div>153 <dl className="mt-3 grid grid-cols-2 gap-y-1 text-[12.5px]">154 <dt className="text-fg-subtle">Normal</dt><dd className="text-right font-mono tabular">{a.baseline_changes_per_day ?? 0} changes/day</dd>155 <dt className="text-fg-subtle">Current</dt><dd className="text-right font-mono tabular">{a.changes_2h ?? 0} in 2 h</dd>156 <dt className="text-fg-subtle">Events 24 h</dt><dd className="text-right font-mono tabular">{a.events_24h ?? 0}</dd>157 <dt className="text-fg-subtle">Events 14 d</dt><dd className="text-right font-mono tabular">{a.events_14d ?? 0}</dd>158 <dt className="text-fg-subtle">Silent 24 h</dt><dd className={`text-right font-mono tabular ${a.silent_24h ? "text-silent" : ""}`}>{a.silent_24h ?? 0}</dd>159 <dt className="text-fg-subtle">Breaking 24 h</dt><dd className={`text-right font-mono tabular ${a.breaking_24h ? "text-hot" : ""}`}>{a.breaking_24h ?? 0}</dd>160 </dl>161 </Panel>162163 <Panel title="Events by type" dense>164 {d.by_type?.length ? (165 <ul className="divide-y divide-line">166 {d.by_type.map((t) => (167 <li key={t.event_type} className="grid grid-cols-[8.5rem_1fr_3rem] items-center gap-2 px-3 py-1.5 text-[12.5px]">168 <Link href={`/live?event_type=${t.event_type}&q=${encodeURIComponent(src.name)}`} className="truncate hover:underline">{typeLabel(t.event_type)}</Link>169 <Bar value={t.n} max={maxType} tone="info" />170 <span className="text-right font-mono text-fg-subtle tabular">{fmtInt(t.n)}</span>171 </li>172 ))}173 </ul>174 ) : (175 <Empty>No events yet.</Empty>176 )}177 </Panel>178179 <Panel title={`Entities · ${d.entities.length}`} dense>180 {d.entities.length ? (181 <ul className="divide-y divide-line">182 {d.entities.map((e) => (183 <li key={e.id} className="flex items-center justify-between gap-2 px-3 py-1.5 text-[13px]">184 <Link href={`/entity/${e.id}`} className="min-w-0 truncate hover:underline">{e.name}</Link>185 <span className="whitespace-nowrap font-mono text-[11px] text-fg-subtle">{e.type.replace(/_/g, " ")} · {e.event_count} ev</span>186 </li>187 ))}188 </ul>189 ) : (190 <Empty>No entities linked.</Empty>191 )}192 </Panel>193194 <Panel title="Source policy">195 <dl className="grid grid-cols-2 gap-y-1 text-[12.5px]">196 <dt className="text-fg-subtle">Provenance</dt><dd className="text-right">{src.first_party === false ? "third-party media" : "first-party channels"}</dd>197 <dt className="text-fg-subtle">robots.txt checked</dt><dd className="text-right font-mono tabular" title={src.robots_checked_at ? utcDateTime(src.robots_checked_at) : undefined}>{src.robots_checked_at ? relTime(src.robots_checked_at) : "—"}</dd>198 <dt className="text-fg-subtle">Importance weight</dt><dd className="text-right font-mono tabular">{src.importance_weight ?? 1}</dd>199 {extra.llm_enabled !== undefined && (200 <>201 <dt className="text-fg-subtle">LLM interpretation</dt><dd className="text-right font-mono">{extra.llm_enabled ? "on" : "off · heuristics only"}</dd>202 </>203 )}204 {extra.rate_limit_per_min ? (205 <>206 <dt className="text-fg-subtle">Rate limit</dt><dd className="text-right font-mono tabular">{extra.rate_limit_per_min}/min</dd>207 </>208 ) : null}209 {extra.discover && Object.values(extra.discover).some(Boolean) && (210 <>211 <dt className="text-fg-subtle">Discovery</dt><dd className="text-right font-mono">{Object.entries(extra.discover).filter(([, v]) => v).map(([k]) => k).join(" · ")}</dd>212 </>213 )}214 {extra.fallback && Object.values(extra.fallback).some(Boolean) && (215 <>216 <dt className="text-fg-subtle">Fallback</dt><dd className="text-right font-mono">{Object.entries(extra.fallback).filter(([, v]) => v).map(([k]) => k).join(" · ")}</dd>217 </>218 )}219 <dt className="text-fg-subtle">Acquisition</dt><dd className="text-right">official feeds first</dd>220 </dl>221 {src.notes && <p className="mt-3 border-t border-line pt-2 text-[11.5px] leading-snug text-fg-muted break-words">{src.notes}</p>}222 </Panel>223 </aside>224 </div>225 </>226 );227}228229function SensorTr({ s }: { s: SensorRow }) {230 const runs = s.total_runs ?? 0;231 const nm = runs ? Math.round(((s.total_not_modified ?? 0) / runs) * 100) : null;232 const prio = s.priority ?? null;233 const cond = [s.has_etag || s.etag ? "etag" : null, s.has_last_modified || s.last_modified ? "lm" : null].filter(Boolean);234 const condTitle = [s.etag ? `ETag: ${s.etag}` : null, s.last_modified ? `Last-Modified: ${s.last_modified}` : null].filter(Boolean).join("\n") || "No conditional-request validators exposed";235 return (236 <tr className="hover:bg-panel-2/60">237 <Td>238 <Link href={`/sensor/${s.id}`} className="font-medium hover:underline">{s.name}</Link>239 <div className="max-w-[16rem] truncate font-mono text-[11px] text-fg-subtle" title={s.url}>{s.url}</div>240 </Td>241 <Td mono><span className="text-fg-muted">{s.type}</span><div className="text-[10.5px] text-fg-subtle">{s.connector}</div></Td>242 <Td><TierBadge tier={s.tier} /></Td>243 <Td>{prio !== null ? <Chip tone={prio === 0 ? "hot" : prio === 1 ? "high" : "default"} className="font-mono" title={PRIORITY_LABELS[prio] ?? `Priority ${prio}`}>P{prio}</Chip> : <span className="text-fg-subtle">—</span>}</Td>244 <Td>{s.status ? <HealthPill health={s.enabled ? s.status : "DISABLED"} /> : <span className="text-fg-subtle">—</span>}</Td>245 <Td><HealthPill health={s.enabled ? s.health : "DISABLED"} /></Td>246 <Td mono className="whitespace-nowrap">247 {s.current_interval_seconds ? fmtDuration(s.current_interval_seconds) : <span className="text-fg-subtle">adaptive</span>}248 <div className="text-[10.5px] text-fg-subtle">{s.base_interval_seconds ? `base ${fmtDuration(s.base_interval_seconds)}` : "no base"}</div>249 </Td>250 <Td mono className="whitespace-nowrap text-fg-subtle">251 <div title={s.last_check_at ? utcDateTime(s.last_check_at) : undefined}>{s.last_check_at ? relTime(s.last_check_at) : "never"}</div>252 <div className="text-[10.5px]" title={s.next_check_at ? utcDateTime(s.next_check_at) : undefined}>{untilTime(s.next_check_at)}</div>253 </Td>254 <Td mono>255 {s.last_status ? <span className={s.last_status >= 500 ? "text-danger" : s.last_status >= 400 ? "text-warn" : s.last_status === 304 ? "text-fg-subtle" : ""}>{s.last_status}</span> : <span className="text-fg-subtle">—</span>}256 {s.last_error ? <div className="max-w-[10rem] truncate text-[10.5px] text-danger" title={s.last_error}>{s.last_error}</div> : null}257 </Td>258 <Td>259 <span className="flex gap-1" title={condTitle}>260 {cond.length ? cond.map((c) => <Chip key={c} tone="ok" className="font-mono">{c}</Chip>) : <span className="text-fg-subtle">—</span>}261 </span>262 </Td>263 <Td mono className="whitespace-nowrap">264 {fmtInt(s.checks_24h ?? 0)} <span className="text-fg-subtle">chk</span>265 <div className="text-[10.5px]">{s.changes_24h ?? 0} <span className="text-fg-subtle">chg</span></div>266 </Td>267 <Td mono>{fmtInt(s.snapshot_count ?? 0)}</Td>268 <Td mono>{nm !== null ? `${nm}%` : <span className="text-fg-subtle">—</span>}</Td>269 <Td mono className="whitespace-nowrap">{s.raw_changes ?? 0} / <span className="text-signal">{s.meaningful_changes ?? 0}</span></Td>270 <Td><Link href={`/sensor/${s.id}`} className="whitespace-nowrap text-[11px] text-info hover:underline">history →</Link></Td>271 </tr>272 );273}274